最近在写类似校园墙的小程序,记录下开发过程。
万丈高楼平地起,在实现一系列的功能之前先要做的就是“登录注册”;因为是小程序注册这块就省略了,只需要写登录的逻辑就好
前端选用了uni-app,一套代码多端发布很吸引人;后端框架选择了SpringBoot,由于是要落地玩的项目,自然是怎么效率高怎么来了
登录模块所需的依赖引入: jwt-token,spring-security
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency>
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.7.1</version> </dependency
|
小程序的登录流程与一般应用没什么区别,只是多存了一个openid而已;登录流程大概如下
1: 前端调用微信的登录API,获取临时code,携带code向后端发送请求
2: 后端接收到请求,携带本次的code,appid(小程序id),secret向微信服务器发送请求,获取唯一标识openid
3: 查找数据库,未发现该openid的条目则创建(实际上就是省略的注册),插入库
4: 签发jwt-token,返回给前端;前端缓存,之后的业务请求携带token进行验证权限
做一些准备:
application.properties中增加配置项
1 2 3
| 小程序id wx.appid=wx683fdea76c3825 wx.secret=0554defd893f3eec9bf4c43dd97
|
注意secret不会被记录,需妥善保存
JWT工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| package com.example.wswbackend.config;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.stereotype.Component;
import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Date; import java.util.UUID;
@Component public class JwtUtil { public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14; public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsffHFdsjkdsfds121232131afasdfac232131afasdfac";
public static String getUUID() { return UUID.randomUUID().toString().replaceAll("-", ""); }
public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID()); return builder.compact(); }
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if (ttlMillis == null) { ttlMillis = JwtUtil.JWT_TTL; }
long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) .setSubject(subject) .setIssuer("sg") .setIssuedAt(now) .signWith(signatureAlgorithm, secretKey) .setExpiration(expDate); }
public static SecretKey generalKey() { byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256"); }
public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(jwt) .getBody(); } }
|
创建用户,权限表
user表:
1 2 3 4 5 6 7 8 9 10 11 12
| create table user ( openid varchar(125) null, wx varchar(24) null, id int auto_increment primary key, role int null, password varchar(300) null, constraint id unique (id) );
|
user_role表
1 2 3 4 5 6 7 8 9
| create table user_role ( id int auto_increment primary key, role_name varchar(100) null, constraint id unique (id) );
|
实体类:
(使用了lombook,省略了一大堆的getset和构造器代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.example.wswbackend.pojo;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;
@Data @AllArgsConstructor @NoArgsConstructor public class User { private String openid; private String wx; @TableId(type = IdType.AUTO) private Integer id; private Integer role; private String password; }
|
配置Security
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
@Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/account/login/", "/user/account/register/").permitAll() .antMatchers(HttpMethod.OPTIONS).permitAll() .anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private UserMapper userMapper;
@Override protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; }
token = token.substring(7);
String userid; try { Claims claims = JwtUtil.parseJWT(token); userid = claims.getSubject(); } catch (Exception e) { throw new RuntimeException(e); }
User user = userMapper.selectById(Integer.parseInt(userid));
if (user == null) { throw new RuntimeException("用户名未登录"); }
UserDetailsImpl loginUser = new UserDetailsImpl(user); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @Data @NoArgsConstructor @AllArgsConstructor public class UserDetailsImpl implements UserDetails {
private User user;
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; }
@Override public String getPassword() { return user.getPassword(); }
@Override public String getUsername() { return user.getOpenid(); }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Service public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired private UserMapper userMapper;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("openid", username); User user = userMapper.selectOne(queryWrapper); if (user == null) { throw new RuntimeException("用户不存在"); } return new UserDetailsImpl(user); } }
|
最后写一个接口组合起来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| @Service public class LoginServiceImpl implements LoginService {
@Autowired private Environment environment;
@Autowired private AuthenticationManager authenticationManager;
@Autowired private UserMapper userMapper;
@Autowired private PasswordEncoder passwordEncoder;
@Override public Map<String, String> login(String code,String wx) { String appid = environment.getProperty("wx.appid"); String secret = environment.getProperty("wx.secret");
Map<String,String> map = new HashMap<>(); String openid = GetOpenIdUtil.getopenid(appid,secret,code); JSONObject jsonObject = JSON.parseObject(openid); openid = jsonObject.getString("openid"); QueryWrapper<User> queryWrapper = new QueryWrapper(); queryWrapper = queryWrapper.eq("openid", openid);
List<User> userList = userMapper.selectList(queryWrapper);
if(userList.isEmpty()){ User user = new User(); user.setRole(0); user.setWx(wx); user.setOpenid(openid); user.setPassword(passwordEncoder.encode(openid)); userMapper.insert(user); }
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(openid,openid); Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken); UserDetailsImpl userDetails = (UserDetailsImpl)authenticate.getPrincipal(); User user = userDetails.getUser(); String jwt = JwtUtil.createJWT(user.getId().toString()); map.put("error_message","success"); map.put("token",jwt); return map; }
|
获取openid的工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class GetOpenIdUtil { public static String getopenid(String appid,String secret,String code) { BufferedReader in = null; String url="https://api.weixin.qq.com/sns/jscode2session?appid=" +appid+"&secret="+secret+"&js_code="+code+"&grant_type=authorization_code"; try{ URL weChatUrl = new URL(url); URLConnection connection = weChatUrl.openConnection(); connection.setConnectTimeout(5000); connection.setReadTimeout(5000); connection.connect(); in = new BufferedReader(new InputStreamReader(connection.getInputStream())); StringBuffer sb = new StringBuffer(); String line; while ((line = in.readLine()) != null) { sb.append(line); } return sb.toString(); }catch (Exception e) { throw new RuntimeException(e); } finally { try { if (in != null) { in.close(); } } catch (Exception e2) { e2.printStackTrace(); } } } }
|
前端部分代码太丑了不做展示,最后实现效果如下图

头像的更新没录到hh